﻿/**
 * @brief 使用 nodejs 创建一个 http server
 */
const http = require('node:http');
const fs = require("node:fs");
const path = require("node:path");
const os = require("node:os");
const _split = ((os.platform() == "win32") ? "\\":"/" );
const CONFIG_DEFAULT = { //默认配置
    Cwd: path.resolve("."+ _split),
    Host:"127.0.0.1", // 主机
    Port:54321, // 端口
    Root:[".","www","html","public"], // 目录
    Index:["index.html"], // 默认主页
    Location:[
        {
            Match:"/",
            Root:[".","www","html","public"],
            Index:["index.html"],
//             Ext:"html",
//             Text:`<!Doctype html>
// <html>
// <head>
// <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
// <meta name="viewport" content="width=device-width, initial-scale=1.0" />
// <title>Wvf app 测试</title>
// <link rel="icon" href="/favicon.ico" type="image/x-icon">
// </head>
// <body>
// </div>
// <script src="/app.js" type="module"></script>
// </body>
// </html>
// `,
        },
        {
            Match:/\\$/,
            Index:["index.html"],
        },
        {
            Match:/.+(\.js|\.js.map)$/,
            Root:["dist"],
        },
        {
            Match:/.+\.ts$/,
            Root:["src"],
            Alias:"/src",
        },
        {
            Match:/.+(\.ico|\.jpg|\.png|\.jpeg|\.gif|\.bmp|\.svg)$/,
        },
        {
            Match:/.*/, //兜底
        }
    ],
    ErrorPage:{
        404 :"Not Found",
        401 :"Unauthorized",
        402 :"Payment Required",
        403 :"Forbidden",
        404 :"Not Found",
        405 :"Method Not Allowed",
        406 :"Not Acceptable",
        407 :"Proxy Authentication Required",
        408 :"Request Timeout",
        409 :"Conflict",
        410 :"Gone",
        411 :"Length Required",
        412 :"Precondition Failed",
        413 :"Content Too Large",
        414 :"URI Too Long",
        415 :"Unsupported Media Type",
        416 :"Range Not Satisfiable",
        417 :"Expectation Failed",
        418 :"I'm a teapot",
        421 :"Misdirected Request",
        422 :"Unprocessable Entity",
        423 :"Locked",
        424 :"Failed Dependency",
        425 :"Too Early",
        426 :"Upgrade Required",
        428 :"Precondition Required",
        429 :"Too Many Requests",
        431 :"Request Header Fields Too Large",
        451 :"Unavailable For Legal Reasons",
        500 :"Internal Server Error",
        501 :"Not Implemented",
        502 :"Bad Gateway",
        503 :"Service Unavailable",
        504 :"Gateway Timeout",
        505 :"HTTP Version Not Supported",
        506 :"Variant Also Negotiates",
        507 :"Insufficient Storage",
        508 :"Loop Detected",
        510 :"Not Extended",
        511 :"Network Authentication Required",
    },
    Mime:{
        'html':'text/html',	'htm':'text/html',	'shtml':'text/html',
        'css':'text/css',		
        'xml':'text/xml',		
        'gif':'image/gif',		
        'jpeg':'image/jpeg',	'jpg':'image/jpeg',	
        'js':'application/javascript',		
        'ts':'application/javascript',	
        'map':'application/javascript',	
        'atom':'application/atom+xml',		
        'rss':'application/rss+xml',		
        'mml':'text/mathml',		
        'txt':'text/plain',		
        'jad':'text/vnd.sun.j2me.app-descriptor',		
        'wml':'text/vnd.wap.wml',		
        'htc':'text/x-component',		
        'avif':'image/avif',		
        'png':'image/png',		
        'svg':'image/svg+xml',	'svgz':'image/svg+xml',	
        'tif':'image/tiff',	'tiff':'image/tiff',	
        'wbmp':'image/vnd.wap.wbmp',		
        'webp':'image/webp',		
        'ico':'image/x-icon',		
        'jng':'image/x-jng',		
        'bmp':'image/x-ms-bmp',		
        'woff':'font/woff',		
        'woff2':'font/woff2',		
        'jar':'application/java-archive',	'war':'application/java-archive',	'ear':'application/java-archive',
        'json':'application/json',		
        'hqx':'application/mac-binhex40',		
        'doc':'application/msword',		
        'pdf':'application/pdf',		
        'ps':'application/postscript',	'eps':'application/postscript',	'ai':'application/postscript',
        'rtf':'application/rtf',		
        'm3u8':'application/vnd.apple.mpegurl',		
        'kml':'application/vnd.google-earth.kml+xml',		
        'kmz':'application/vnd.google-earth.kmz',		
        'xls':'application/vnd.ms-excel',		
        'eot':'application/vnd.ms-fontobject',		
        'ppt':'application/vnd.ms-powerpoint',		
        'odg':'application/vnd.oasis.opendocument.graphics',		
        'odp':'application/vnd.oasis.opendocument.presentation',		
        'ods':'application/vnd.oasis.opendocument.spreadsheet',		
        'odt':'application/vnd.oasis.opendocument.text',		
        'pptx':'application/vnd.openxmlformats-officedocument.presentationml.presentation',		
        'xlsx':'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',		
        'docx':'application/vnd.openxmlformats-officedocument.wordprocessingml.document',		
        'wmlc':'application/vnd.wap.wmlc',		
        'wasm':'application/wasm',		
        '7z':'application/x-7z-compressed',		
        'cco':'application/x-cocoa',		
        'jardiff':'application/x-java-archive-diff',		
        'jnlp':'application/x-java-jnlp-file',		
        'run':'application/x-makeself',		
        'pl':'application/x-perl',	'pm':'application/x-perl',	
        'prc':'application/x-pilot',	'pdb':'application/x-pilot',	
        'rar':'application/x-rar-compressed',		
        'rpm':'application/x-redhat-package-manager',		
        'sea':'application/x-sea',		
        'swf':'application/x-shockwave-flash',		
        'sit':'application/x-stuffit',		
        'tcl':'application/x-tcl',	'tk':'application/x-tcl',	
        'der':'application/x-x509-ca-cert',	'pem':'application/x-x509-ca-cert',	'crt':'application/x-x509-ca-cert',
        'xpi':'application/x-xpinstall',		
        'xhtml':'application/xhtml+xml',		
        'xspf':'application/xspf+xml',		
        'zip':'application/zip',		
        'bin':'application/octet-stream',	'exe':'application/octet-stream',	'dll':'application/octet-stream',
        'deb':'application/octet-stream',		
        'dmg':'application/octet-stream',		
        'iso':'application/octet-stream',	'img':'application/octet-stream',	
        'msi':'application/octet-stream',	'msp':'application/octet-stream',	'msm':'application/octet-stream',
        'mid':'audio/midi',	'midi':'audio/midi',	'kar':'audio/midi',
        'mp3':'audio/mpeg',		
        'ogg':'audio/ogg',		
        'm4a':'audio/x-m4a',		
        'ra':'audio/x-realaudio',		
        '3gpp':'video/3gpp',	'3gp':'video/3gpp',	
        'ts':'application/ts',		
        'mp4':'video/mp4',		
        'mpeg':'video/mpeg',	'mpg':'video/mpeg',	
        'mov':'video/quicktime',		
        'webm':'video/webm',		
        'flv':'video/x-flv',		
        'm4v':'video/x-m4v',		
        'mng':'video/x-mng',		
        'asx':'video/x-ms-asf',		'asf':'video/x-ms-asf',	
        'wmv':'video/x-ms-wmv',	
        'avi':'video/x-msvideo',	
    },
};

/**
 * 分析 命令行参数 处理参数
 * 参数 格式如下: 
 *  -x -y # 无后缀参数
 *  -xy # 无后缀参数，可以合并
 *  -x arg 或 --xx arg // 有后缀参数
 *  -
 * @param {string[]} argv 来自 process.argv.slice(1); 的返回值
 * @param { {
 *  sname?:string, // 短线后的字符
 *  lname?:string, // 长线后的字符
 *  arg?:boolean, // 是否跟有 arg, 默认为 false
 *  validator?:(arg:string)=>boolean, //验证函数 返回函数是否合法
 *  default?:string, //默认值，
 *  translator:{fun:(arg:string, dst:any)=>void,dst:any}, //转换参数为对应的数据，填写到对应的目标中
 *  actuator?:{fun:(arg:string, ...params:any)=>any,params:any}, // 直接执行的函数
 *  help:string, //本命令帮助参数
 * }[] 
 * } configs getopt的配置
 * @returns { {
 * sname?:string, // 短线后的字符
 * lname?:string, // 长线后的字符
 * value?:string, //值
 * }[] | {ERROR:number, }
 * }
 */
function getopt(argv, configs){
    /**
     * @type {string[]} process.argv.slice(1); 的返回值
     */
    let _argv;
    /**
     * @type {{
    * sname?:string, // 短线后的字符
    * lname?:string, // 长线后的字符
    * value?:string, //值
    * }[]}
     */
    let ret = [];
    if(typeof argv[0] == "string" ){
        let basename = path.basename(argv[0]);
        if(basename == "node" || basename == "node.exe"){
            _argv = argv.slice(2);
        }else{
            _argv = argv.slice(1);
        }
    }
    for(let i = 0; i < _argv.length; i++){
        if(_argv[i].startsWith("--")){ //以 -- 开始
            const _a = _argv[i].slice(2);
            const r = hhh(ggg(_a, 2),i, _a) ;
            if(r["ERROR"]){return r;}
            ret.push(r);
        }else if(_argv[i].startsWith("-")){ // 以 - 开始
            const _a = _argv[i].slice(1);
            if(_a.length > 1){
                for(const k = 0; k != _a.length; k++){
                    const r = hhh(ggg(_a[k],1),i, _a[k]);
                    if(r["ERROR"]){return r;}
                    if(r._iplugplug){i++;}
                    ret.push(r);
                }
            }else{
                const r = hhh(ggg(_a,1),i,_a);
                if(r["ERROR"]){return r;}
                if(r._iplugplug){i++;}
                ret.push(r);
            }
        }
    }
    for(let i = 0; i != configs.length; i++){
        const _c = configs[i];
        if(_c._done != true && _c.default){
            if(_c.validator && _c.validator(_c.default) == false){return {ERROR:i, reason:`arg err after ${_a}`};}
            if(_c.translator && _c.translator.fun){_c.translator.fun(_c.default,_c.translator.dst);}
            if(_c.actuator && _c.actuator.fun){_c.actuator.fun(_c.default,_c.actuator.params);}
            ret.push({sname:_c.sname,  lname:_c.lname,  value:_c.default, });
        }
    }
    return ret;
    function ggg(_a, t = 2){
        const _A = {1:"sname",2:"lname"};
        for(let i = 0; i != configs.length; i++){
            if(configs[i][_A[t]]  == _a){
                return i;
            }
        }
    }
    function hhh(j, i, _a){
        if(j == undefined) {return {ERROR:i, reason:`cant't find argv ${_a}`};}
        const _c = configs[j];
        let arg;
        if(_c.arg == true) {
            if(i >= _argv.length ){return {ERROR:i, reason:`no arg after ${_a}`};}
        }
        arg =  _argv[i+1];
        if(_c.validator && _c.validator(arg) == false){return {ERROR:i, reason:`arg err after ${_a}`};}
        if(_c.translator && _c.translator.fun){_c.translator.fun(arg,_c.translator.dst);}
        if(_c.actuator && _c.actuator.fun){_c.actuator.fun(arg,_c.actuator.params);}
        _c._done = true;
        return {sname:_c.sname,  lname:_c.lname,  value:arg, _iplugplug:arg?true:false};
    }
}
// /**
//  * 使用 glob模式检查 str 的匹配 类似于 man 3 glob
//  * 
//     / to separate path segments
//     * to match zero or more characters in a path segment
//     ? to match on one character in a path segment
//     ** to match any number of path segments, including none
//     {} to group conditions (for example {** / *.html, ** /*.txt} matches all HTML and text files)
//     [] to declare a range of characters to match (example.[0-9] to match on example.0, example.1, …)
//     [!...] to negate a range of characters to match (example.[!0-9] to match on example.a, example.b, but not example.0)

//  * @param {string} _path // 源字符串的路径，
//  * @param {string} match // glob字符串
//  * @returns {} 返回 []
//  */
// function glob(_path, match){
//     let r = [];
    
// }

function httpd(config){
    function getErrorPage(err){
        const last = [500,CONFIG_DEFAULT.ErrorPage[500], "html"];
        const errpage = config.ErrorPage[err]??CONFIG_DEFAULT.ErrorPage[err];
        if (!errpage){return last; }
        let text,ext = "";
        if(errpage.File){
            try{
                text = fs.readFileSync(config.ErrorPage[err].File);
                ext = path.extname(config.ErrorPage[err].File);
            }catch(e){ }
        }else{
            text = errpage;
            ext = "html";
        }
        if(!text){return last;}
        return [err, text, ext]
    }

    function readFileSync(file, root){
        let text,ext;
        if(root){
            for(let i = 0; i != root.length; i++){
                const _p = path.resolve(root[i] , file.startsWith("/")?file.slice(1):file);
                if(_p.startsWith(config.Cwd)){
                    try{
                        text = fs.readFileSync( _p );
                        ext = path.extname(file).slice(1);
                        break;
                    }catch(e){ }
                }
            }
        }else{
            const _p = path.resolve(file.startsWith("/")?file.slice(1):file);
            if(_p.startsWith(config.Cwd)){
                try{
                    text = fs.readFileSync( file);
                    ext = path.extname(file).slice(1);
                }catch(e){ }                 
            }
        }
        if(text === undefined ) {return getErrorPage(404);}
        return [200, text, ext];

    }
    const server = http.createServer((req, res) => {
        console.log("Req:",req.url);
        const [_url, qurey] = req.url.split("?");
        for(let i = 0;i != config.Location.length; i++){
            if((config.Location[i].Match instanceof RegExp && _url.match(config.Location[i].Match)) || config.Location[i].Match == _url){
                let text,ext,code;
                if(config.Location[i].Text){
                    code = 200,text = config.Location[i].Text,  ext = config.Location[i].Ext??"html";
                }else{
                    let {dir,base} = path.parse(_url);
                    const Root = config.Location[i].Root ?? config.Root;
                    if(base == ""){ // _url中没有文件,仅仅是一个路径
                        const Index = (config.Location[i].Index ?? config.Index);
                        for(let i = 0; i != Index.length;i++){
                            [code, text, ext] = readFileSync(Index[i],Root);
                            if(code){ break; }
                        }
                    }else{
                        [code, text, ext] = readFileSync((config.Location[i].Alias && (config.Location[i].Alias == dir)?base : _url), Root);
                    }
                }
                res.statusCode = code;
                res.setHeader('Content-Type', config.Mime[ext]??"application/octet-stream");
                res.setHeader('Content-Length', text.length);
                // res.setHeader('Access-Control-Allow-Origin', "*");
                res.end(text);
                return;
            }
        }
        res.statusCode = 500;
        res.setHeader('Content-Type', CONFIG_DEFAULT.Mime["txt"]);
        res.setHeader('Content-Length', CONFIG_DEFAULT.ErrorPage[500].length);
        res.end( CONFIG_DEFAULT.ErrorPage[500]);
        return;
    });
    
    server.listen(config.Port, config.Host, () => {
      console.log(`Server running at http://${config.Host}:${config.Port}/ Current Path:${config.Cwd}`);
    });
}
let config = {};
function setConfigDefault(config){
    if(! config.Root){config.Root = CONFIG_DEFAULT.Root;}
    if(! config.Cwd){config.Cwd = CONFIG_DEFAULT.Cwd;}
    if(! config.Host){config.Host = CONFIG_DEFAULT.Host;}
    if(! config.Port){config.Port = CONFIG_DEFAULT.Port;}
    if(! config.Index){config.Index = CONFIG_DEFAULT.Index;}
    if(! config.Location){config.Location = CONFIG_DEFAULT.Location;}
    if(! config.ErrorPage){config.ErrorPage = CONFIG_DEFAULT.ErrorPage;}
    if(! config.Mime){config.Mime = CONFIG_DEFAULT.Mime;}
    fixPath(config.Root);
    for(let i = 0; i != config.Location.length; i++){
        if(!config.Location[i].Root){
            config.Location[i].Root = [].concat(config.Root);
        }else{
            config.Location[i].Root = config.Location[i].Root.concat(config.Root);
        }
        if(config.Location[i].Root){fixPath(config.Location[i].Root);}
    }
    
    /**
     * 
     * @param {string[]} paths 
     */
    function fixPath(paths){
        for(let i = 0; i != paths.length; i++){
            if(!paths[i].endsWith(_split) ){ paths[i]+= _split;}
        }        
    }
}
function print_help(){
    console.clear();
    console.log("node httpd.js Usage:\n -p/--path 执行路径;\n-c/--config 配置路径;\n-h/--help 显示本帮助; 例 node httpd.js -p ../bin\n");
    process.exit(0);
}
let opt = getopt(process.argv, [
    {
        sname:"p",
        lname:"path",
        arg:true,
        actuator:{fun:(_p)=>{
            process.chdir(_p);
            CONFIG_DEFAULT.Cwd = process.cwd() + _split;
        }}
    },
    {
        sname:"c",
        lname:"config",
        arg:true,
        translator:{
            fun:(file,config)=>{ 
                try{
                    let json = fs.readFileSync(file);
                    config = JSON.parse(json);
                }catch(e){
                    console.log("ERROR:",e.message,);
                    process.exit(1);
                }
            },
            dst:config,
        }
    },
    {
        sname:'h',
        lname:"help",
        arg:false,
        actuator:{fun:print_help}
    }
]);
if(opt.length == 0){
    print_help();
}else if(!config["ERROR"]){
    setConfigDefault(config);
    httpd(config);
}